<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Three.js Offline Map</title>
    <style>
        body { margin: 0; overflow: hidden; }
    </style>
</head>
<body>

<!-- 引入 Three.js -->
<script type="importmap">
    {
        "imports": {
            "three": "./three.module.js",
            "three/addons/": "./jsm/"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    // --- 1. 场景初始化 ---
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // --- 2. 地图参数和状态 ---
    const mapConfig = {
        tileSize: 256, // 瓦片像素尺寸
        rootUrl: './offline_tiles/', // 离线瓦片根目录
        minZoom: 12,   // 你下载的最小层级
        maxZoom: 15,   // 你下载的最大层级
        center: { lng: 114.5, lat: 30.5 }, // 地图中心点（与你下载的区域对应）
        zoom: 12       // 初始缩放层级
    };

    // 用于存储已加载的瓦片对象 { key: mesh }
    const loadedTiles = new Map();

    // --- 3. 核心：瓦片加载和管理 ---

    /**
     * 将经纬度转换为瓦片坐标
     */
    function latLngToTile(lat, lng, zoom) {
        const lat_rad = lat*(Math.PI/180)
        const n = Math.pow(2, zoom);
        const x = Math.floor((lng + 180) / 360 * n);
        const y = Math.floor((1.0 - Math.asinh(Math.tan(lat_rad)) / Math.PI) / 2.0* n);
        return { x, y };
    }

    /**
     * 将瓦片坐标转换为 3D 世界坐标
     */
    function tileToWorld(x, y, zoom) {
        const centerTile = latLngToTile(mapConfig.center.lat, mapConfig.center.lng, zoom);
        const offsetX = (x - centerTile.x) * mapConfig.tileSize;
        const offsetY = (y - centerTile.y) * mapConfig.tileSize;
        return { x: offsetX, y: -offsetY };
    }

    /**
     * 加载并显示当前视野内的瓦片
     */
    function updateVisibleTiles() {
        // 简单粗暴：先清除所有已加载的瓦片
        // 在实际应用中，应只移除视野外的瓦片以优化性能
        loadedTiles.forEach(mesh => scene.remove(mesh));
        loadedTiles.clear();

        // 获取当前相机的缩放层级（这是一个估算值，更精确的方法是通过射线投射）
        // 这里我们简化处理，直接使用一个全局 zoom 变量
        const currentZoom = Math.floor(mapConfig.zoom);
        
        // 确保层级在有效范围内
        if (currentZoom < mapConfig.minZoom || currentZoom > mapConfig.maxZoom) {
            return;
        }

        // 计算中心点对应的瓦片坐标
        const centerTile = latLngToTile(mapConfig.center.lat, mapConfig.center.lng, currentZoom);

        // 定义需要加载的瓦片范围（视野范围）
        const viewRange = 2; // 视野中心周围各加载 2 个瓦片

        const loader = new THREE.TextureLoader();
        // 循环加载瓦片
        for (let x = centerTile.x - viewRange; x <= centerTile.x + viewRange; x++) {
            for (let y = centerTile.y - viewRange; y <= centerTile.y + viewRange; y++) {
                const tileUrl = `${mapConfig.rootUrl}${currentZoom}/${x}/${y}.png`;
                const tileKey = `${currentZoom}/${x}/${y}`;

                loader.load(
                    tileUrl,
                    (texture) => {
                        // 纹理加载成功
                        const material = new THREE.MeshBasicMaterial({ map: texture });
                        const geometry = new THREE.PlaneGeometry(mapConfig.tileSize, mapConfig.tileSize);
                        const tileMesh = new THREE.Mesh(geometry, material);

                        // 设置瓦片在世界中的位置
                        const worldPos = tileToWorld(x, y, currentZoom);
                        tileMesh.position.set(worldPos.x, worldPos.y,0);
                        
                        // 旋转瓦片使其正面朝上
                        // tileMesh.rotation.x = -Math.PI / 2;

                        scene.add(tileMesh);
                        loadedTiles.set(tileKey, tileMesh);
                    },
                    undefined, // onProgress
                    (error) => {
                        // 瓦片加载失败（例如，该位置没有瓦片数据）
                        console.warn(`瓦片加载失败或不存在: ${tileUrl}`, error);
                    }
                );
            }
        }
    }

    // --- 4. 相机、控制器和初始位置 ---
    // 设置相机初始高度
    camera.position.z = 1200;

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.maxPolarAngle = Math.PI / 2; // 限制垂直旋转，防止地图翻转

    // 监听控制器变化，更新地图中心并重新加载瓦片
    // 注意：这是一个简化的实现，实际应用中需要更复杂的逻辑来更新 mapConfig.center
    controls.addEventListener('change', () => {
        // 这里我们简单地通过相机高度来模拟 zoom 变化
        mapConfig.zoom = mapConfig.minZoom - (camera.position.z - 1000) / 100; // 一个粗略的映射
        mapConfig.zoom = Math.max(mapConfig.minZoom, Math.min(mapConfig.maxZoom, mapConfig.zoom));
        
        // 当用户停止交互时更新瓦片
        if (!controls.isRotating && !controls.isPanning && !controls.isZooming) {
            updateVisibleTiles();
        }
        console.log(mapConfig.zoom)
    });

    // --- 5. 初始化和渲染 ---
    // 首次加载瓦片
    updateVisibleTiles();

    function animate() {
        requestAnimationFrame(animate);
        controls.update();
        renderer.render(scene, camera);
    }
    animate();

    // --- 6. 窗口大小调整 ---
    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });
</script>

</body>
</html>